<!DOCTYPE html>
<html lang="en-US">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>RPC-Framework Monitor</title>
  <!--<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">-->
  <link href="./dist/css/bootstrap.min.css" rel="stylesheet">
  <!--<link rel="stylesheet" href="./dist/css/bootstrap-icons.min.css">-->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
  <style>
    #status-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin-right: 8px; }
    @media (max-width: 600px) {
      .card { margin-bottom: 1rem; }
      #config-btn { width: 100%; }
    }
    #logs-panel {
        height: 100dvh;
        white-space: pre-wrap;       /* Since CSS 2.1 */
        white-space: -moz-pre-wrap;  /* Mozilla, since 1999 */
        white-space: -pre-wrap;      /* Opera 4-6 */
        white-space: -o-pre-wrap;    /* Opera 7 */
        word-wrap: break-word;       /* Internet Explorer 5.5+ */
        max-height: 600px;
    }
    .card.text-center:hover {
        box-shadow: 0 0 10px 2px #0d6efd33; /* subtle blue shadow */
        background-color: #c1c4c7;           /* light blue background */
        transition: box-shadow 0.2s, background-color 0.2s;
        cursor: pointer;
    }
    #setpvModal {
        z-index: 1060 !important;
    }
    #setpvModal-backdrop {
        z-index: 1059 !important;
    }

    #timeSeriesModal {
        z-index: 1070 !important;
    }
    #timeSeriesModal-backdrop {
        z-index: 1069 !important;
    }
    #logContextMenu {
        min-width: 120px;
    }
    .table-striped > tbody > tr:nth-of-type(odd) {
        --bs-table-accent-bg: #4a76a5; /* Light blue, or any color you like */
        background-color: var(--bs-table-accent-bg) !important;
    }
    .dropdown-item.disabled {
        pointer-events: none;
        opacity: 0.6;
    }
  </style>
</head>
<!--<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>-->
<script src="./dist/js/bootstrap.bundle.min.js"></script>
<body class="bg-light">
  <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
    <div class="container-fluid">
      <span id="status-dot" class="bg-success"></span>
      <span class="navbar-brand mb-0 h1" id="APP_TITLE">RPC-Framework Monitor</span>
      <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarNav">
        <ul class="navbar-nav ms-auto">
          <li class="nav-item">
            <!--<button id="config-btn" class="btn btn-light btn-sm" data-bs-toggle="modal" data-bs-target="#configModal">Update Config</button>-->
            <span id="user-label" class="ms-2 text-white"></span>
            <button id="reconnect-btn" class="btn btn-warning btn-sm ms-2 d-none">Login</button>
          </li>
        </ul>
      </div>
    </div>
  </nav>

<main class="container my-3">
  <!-- Banner with site-tree dropdown -->
  <div class="d-flex align-items-center mb-3">
    <div class="me-auto">
      <div class="dropdown">
        <button class="btn btn-outline-primary btn-sm dropdown-toggle" id="siteTreeDropdownBtn"
                data-bs-toggle="dropdown" aria-expanded="false">
          Sites
        </button>
        <div class="dropdown-menu p-2" aria-labelledby="siteTreeDropdownBtn" style="min-width:260px;">
          <div id="siteTreeContainer" class="site-tree"></div>
        </div>
      </div>
    </div>
    <!--<div>
      <button id="expandAllSitesBtn" class="btn btn-sm btn-light me-2">Expand All</button>
      <button id="collapseAllSitesBtn" class="btn btn-sm btn-light">Collapse All</button>
    </div>-->
  </div>

  <div class="row" id="sites-row">
    <!-- one site card displayed at a time -->
  </div>

  <div class="row" id="logs-row" style="display:none;">
    <div class="col-12">
      <div class="card mb-3">
        <div class="col-2" class="card-header d-flex justify-content-between align-items-center" id="LogsTitle">
          <span id="LogsTitleText">Logs</span>
          <a id="toggle-logs-link" href="#" style="text-decoration:none; cursor:pointer;">[+]</a>
          </div>
        </div>
        <div class="card-body" id="logs-body" style="display:none; overflow-y:auto;">
          <pre id="logs-panel" class="mb-0">Loading logs...</pre>
        </div>
      </div>
    </div>
  </div>
</main>

<!--<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>-->
<script src="./dist/js/chart.js"></script>
  <!-- Time Series Modal -->
<div class="modal fade" id="timeSeriesModal" tabindex="-1" aria-labelledby="timeSeriesModalLabel" aria-hidden="true">
  <div class="modal-dialog modal-lg">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title" id="timeSeriesModalLabel">Time Series</h5>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <canvas id="timeSeriesChart" height="200"></canvas>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="timeseries-dlg-close-btn">Close</button>
      </div>
    </div>
  </div>
</div>

  <!-- Set Point Values Modal -->
  <div class="modal fade" id="setpvModal" tabindex="-1" aria-labelledby="setpvModalLabel" aria-hidden="true">
    <div class="modal-dialog">
      <form class="modal-content" id="setpv-form">
        <div class="modal-header">
          <h5 class="modal-title" id="setpvModalLabel">Set Point Values</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <div class="mb-3">
            <label for="setpv-param" class="form-label">Point Name</label>
            <input type="text" class="form-control" id="setpv-param" required>
          </div>
          <div class="mb-3">
            <label for="setpv-value" class="form-label">Value</label>
            <input type="text" class="form-control" id="setpv-value" required>
          </div>
          <div id="setpv-feedback" class="text-danger small"></div>
        </div>
        <div class="modal-footer">
          <button type="submit" class="btn btn-primary" id="setpv-dlg-btn-update">Update</button>
          <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="setpv-dlg-btn-cancel">Cancel</button>
        </div>
      </form>
    </div>
  </div>

  <!-- Setpoints Modal -->
  <div class="modal fade" id="setpointsModal" tabindex="-1" aria-labelledby="setpointsModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="setpointsModalLabel">Setpoints</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
        </div>
        <div class="modal-body">
          <h6 id="setpointsAppName" class="mb-3"></h6>
          <table class="table table-striped">
            <thead>
              <tr>
                <th scope="col" id="setpoint-name-header">Name</th>
                <th scope="col" id="setpoint-value-header">Value</th>
                <th scope="col" id="setpoint-type-header">Point Type</th>
              </tr>
            </thead>
            <tbody id="setpointsTableBody">
              <tr>
                <td colspan="2">Loading...</td>
              </tr>
            </tbody>
          </table>
        </div>
        <div class="modal-footer">
          <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="setpoints-dlg-close-btn">Close</button>
        </div>
      </div>
    </div>
  </div>
    <!-- Custom Context Menu for Log Time Range -->
  <ul id="logContextMenu" class="dropdown-menu" style="position:absolute; display:none; z-index:2000;">
    <li><a class="dropdown-item" href="#" data-range="1h">Last 200 samples</a></li>
    <li><a class="dropdown-item" href="#" data-range="2h">Last 2000 samples</a></li>
    <li><a class="dropdown-item" href="#" data-range="3h">Last 20000 samples</a></li>
    <li><a class="dropdown-item" href="#" data-range="full">Full Log</a></li>
  </ul>

    <ul id="appContextMenu" class="dropdown-menu" style="position:absolute; display:none; z-index:2000;">
      <li><a class="dropdown-item" href="#" data-action="start">Start</a></li>
      <li><a class="dropdown-item" href="#" data-action="stop">Stop</a></li>
      <li><a class="dropdown-item" href="#" data-action="restart">Restart</a></li>
    </ul>

  <script src="./i18n-helper.js"></script>

  <footer class="text-center text-muted py-3">
    rpc-frmwrk &copy; 2025
  </footer>
  <script src="./appdetail.js"></script>
  <!--p id="log">RPC-Framework Monitor<br /></p>-->
  <script src="dist/appmon.js"></script>

  <script>
    const i18nh = globalThis.i18nHelper; // Make it globally accessible
    i18nh.load(navigator.language).then(() => {
      console.log("Translations loaded");
      document.getElementById('reconnect-btn').textContent = i18nh.t("Login");
      document.getElementById('APP_TITLE').textContent = i18nh.t("APP_TITLE");
      document.getElementById('logs-panel').textContent = i18nh.t("Loading logs...");
      document.getElementById('LogsTitleText').textContent = i18nh.t("LOGS");
      document.getElementById('setpv-dlg-btn-update').textContent = i18nh.t("BUTTON_UPDATE");
      document.getElementById('setpv-dlg-btn-cancel').textContent = i18nh.t("BUTTON_CANCEL");
      document.getElementById("setpoints-dlg-close-btn").textContent = i18nh.t("BUTTON_CLOSE");
      document.getElementById("timeseries-dlg-close-btn").textContent = i18nh.t("BUTTON_CLOSE");
      document.getElementById("siteTreeDropdownBtn").textContent = i18nh.t("BUTTON_SITES");
      document.querySelector('#logContextMenu [data-range="1h"]').textContent = i18nh.t("LAST_200_SAMPLES");
      document.querySelector('#logContextMenu [data-range="2h"]').textContent = i18nh.t("LAST_2000_SAMPLES");
      document.querySelector('#logContextMenu [data-range="3h"]').textContent = i18nh.t("LAST_20000_SAMPLES");
      document.querySelector('#logContextMenu [data-range="full"]').textContent = i18nh.t("FULL_LOG");

      document.querySelector('#appContextMenu [data-action="start"]').textContent = i18nh.t("MENU_START");
      document.querySelector('#appContextMenu [data-action="stop"]').textContent = i18nh.t("MENU_STOP");
      document.querySelector('#appContextMenu [data-action="restart"]').textContent = i18nh.t("MENU_RESTART");
    }).catch((error) => {
      console.error("Error loading translations:", error);
    });

    function setStatusDot(isOnline) {
      const dot = document.getElementById('status-dot');
      dot.classList.remove('bg-success', 'bg-danger');
      if (isOnline === true) {
        dot.classList.add('bg-success');
      } else if (isOnline === false) {
        dot.classList.add('bg-danger');
      }
    }

    const oStartCb = ((context, ret)=>{
      setStatusDot(true);
      showReconnectButton(true, true);
      globalThis.curProxy = globalThis.rootProxy;
      // renderSite( globalThis.GetSite(
      //     globalThis.curProxy.m_strRouterPath ) );
      initSiteTree()
      return fetchStatus().then((ret)=>{
          return fetchLogs().then((ret)=>{
              globalThis.fetchStatusInterval =
                  setInterval(fetchStatus, 5000);
              globalThis.fetchLogsInterval =
                  setInterval(fetchLogs, 30000);
          })
      })
    })


    /**
     * Clear site tree and related UI/state when user logs out.
     */
    function clearSiteTree() {
        try {
            // Clear DOM
            const tree = document.getElementById('siteTreeContainer');
            if (tree) tree.innerHTML = '';

            const sitesRow = document.getElementById('sites-row');
            if (sitesRow) sitesRow.innerHTML = '';

        } catch (e) {
            console.error("clearSiteTree error:", e);
        }
    }


    function showReconnectButton(show, isConnected = false) {
        const btn = document.getElementById('reconnect-btn');
        const userLabel = document.getElementById('user-label');

        if (show) {
            btn.classList.remove('d-none');
            if (isConnected) {
                btn.textContent = i18nh.t('Logout');
                btn.classList.remove('btn-warning');
                btn.classList.add('btn-danger');
                var strUser; 
                var curProxy = globalThis.curProxy;
                if( !curProxy )
                    strUser = ""
                else if( curProxy && curProxy.m_strUserName )
                    strUser = curProxy.m_strUserName;
                else if( curProxy && curProxy.m_oChanProxy &&
                    curProxy.m_oChanProxy.m_strUserName )
                    strUser = curProxy.m_oChanProxy.m_strUserName;

                userLabel.textContent = strUser;
                btn.onclick = function() {
                    // Your disconnect logic here
                    if (globalThis.rootProxy ) {
                        globalThis.rootProxy.Stop();
                    } else {
                      // fallback: reload or nullify
                        location.reload();
                    }
                };

                const logsRow = document.getElementById('logs-row');
                if (logsRow) logsRow.style.display = "";

            } else {
                btn.textContent = i18nh.t('Login');
                userLabel.textContent = '';
                btn.classList.remove('btn-danger');
                btn.classList.add('btn-warning');
                btn.onclick = function() {
                  // Your reconnect logic here
                if (typeof globalThis.StartClient === 'function') {
                    globalThis.StartClient( oStartCb ).then(() => {
                        console.log('Reconnected with status ');
                        checkProxyState()
                    }).catch((error) => {
                        console.log("Error reconnecting:", error);
                    });
                    } else {
                        location.reload();
                    }
                };
                const logsRow = document.getElementById('logs-row');
                if (logsRow) logsRow.style.display = "none";
            }
        } else {
            btn.classList.add('d-none');
            userLabel.textContent = '';
        }
    }

    setStatusDot(false);
    showReconnectButton(true);

    function initRootSite()
    {
        globalThis.g_rootSite = {
            name: "site1",
            container: "container1",
            status: "online",
            routerPath: "/site1",
            apps: [
            /*{ name: "AppA", status: "Running", cpu: "10%" },
            { name: "AppB", status: "Stopped", cpu: "0%" } */
            ]
        }
        globalThis.g_rootSite.children = new Map();
    }
    initRootSite();

    function getDisplayName(app) {
        if( app.display_name && app.display_name.length > 0 )
            return i18nHelper.t(app.display_name);
        return i18nHelper.t(app.name);
    }
    function updateSite( site = null ) {
        var curProxy = globalThis.curProxy;
        if( curProxy === null )
            return;
        if( site === null )
            site = globalThis.GetSite( curProxy.m_strRouterPath ); 
        if( globalThis.curSpModal )
        {
            updateAppDetailModal( globalThis.curSpModal.currentApp,
                globalThis.curSpModal.currentSite );
        }
        if( !globalThis.curSpModal || !curProxy || curProxy.m_iState === 0 )
        {
            const sitesRow = document.getElementById('sites-row');
            var siteCard = null;
            {
                appRow = document.getElementById(`app-row-${site.name}`);
                if( !appRow )
                    return;
                
                if( appRow.children.length !== site.apps.length )
                {
                    renderSite(site);
                    return;
                }
                for( const app of site.apps ) {
                    appTitle = document.getElementById(`app-title-${app.name}-${site.name}`);
                    appTitle.textContent = getDisplayName(app);
                    appStatus = document.getElementById(`app-status-${app.name}-${site.name}`)
                    if( appStatus )
                    {
                        let statusClass = '';
                        if (app.status === i18nHelper.t("APP_STATUS_RUNNING")) {
                              statusClass = 'bg-success text-white px-2 rounded';
                        } else if (app.status === i18nHelper.t("APP_STATUS_STOPPED")) {
                              statusClass = 'bg-danger text-white px-2 rounded';
                        } else if (app.status === i18nHelper.t("APP_STATUS_UNKNOWN")) {
                              statusClass = 'bg-secondary text-white px-2 rounded';
                        }
                        appStatus.innerHTML = `<b>${i18nHelper.t("STATUS")}:</b> <span class="${statusClass}">${app.status}</span><br><b>${i18nHelper.t("CPU_USAGE")}:</b> ${app.cpu}`;
                    }
                }
            }
        }
    }

    function renderSite(site) {
        const sitesRow = document.getElementById('sites-row');
        sitesRow.innerHTML = ''; // Clear previous content

        if( site )
        {
            // Create site card
            const siteCol = document.createElement('div');
            siteCol.className = 'col-12 mb-4';

            const siteCard = document.createElement('div');
            siteCard.className = 'card';

            const siteHeader = document.createElement('div');
            siteHeader.className = 'card-header bg-info text-white';
            const oParams = globalThis.GetSiteParams( site.routerPath );
            var loc;
            if( oParams )
                loc = oParams.IpAddress
            else
                loc = (new URL( globalThis.rootProxy.m_strUrl)).hostname
            // Build header with italicized location
            siteHeader.textContent = ''; // clear any previous value
            const headerPrefix = document.createElement('span');
            headerPrefix.textContent = `${i18nHelper.t("SITE_LABEL")}: ${site.name} (${i18nHelper.t("CONTAINER_LABEL")}: ${site.routerPath} `;
            const locEm = document.createElement('em');
            locEm.textContent = loc;
            const closing = document.createTextNode(')');
            siteHeader.appendChild(headerPrefix);
            siteHeader.appendChild(locEm);
            siteHeader.appendChild(closing);

            const siteBody = document.createElement('div');
            siteBody.className = 'card-body';

            const appRow = document.createElement('div');
            appRow.className = 'row';
            appRow.id = `app-row-${site.name}`;

            site.apps.forEach(app => {
                const appCol = document.createElement('div');
                appCol.className = 'col-12 col-md-6 col-lg-3 mb-3';

                const appCard = document.createElement('div');
                appCard.className = 'card text-center';
                appCard.style.cursor = 'pointer';

                // Right-click to show app context menu
                appCard.oncontextmenu = function(e) {
                    e.preventDefault();
                    showAppContextMenu(e, app, site);
                };
                appCard.onclick = function() {
                    if( !globalThis.curSpModal && globalThis.curProxy )
                        showAppDetailModal(app, site);
                }

                const appCardBody = document.createElement('div');
                appCardBody.className = 'card-body';

                const appTitle = document.createElement('h5');
                appTitle.className = 'card-title';
                appTitle.id = `app-title-${app.name}-${site.name}`;
                appTitle.textContent = getDisplayName(app);

                const appStatus = document.createElement('p');
                appStatus.className = 'card-text';
                appStatus.id = `app-status-${app.name}-${site.name}`;

                let statusClass = '';
                if (app.status === i18nHelper.t("APP_STATUS_RUNNING")) {
                    statusClass = 'bg-success text-white px-2 rounded';
                } else if (app.status === i18nHelper.t("APP_STATUS_STOPPED")) {
                    statusClass = 'bg-danger text-white px-2 rounded';
                } else if (app.status === i18nHelper.t("APP_STATUS_UNKNOWN")) {
                    statusClass = 'bg-secondary text-white px-2 rounded';
                }
                appStatus.innerHTML = `<b>${i18nHelper.t("STATUS")}:</b> \
                    <span class="${statusClass}">${app.status}</span><br> \
                    <b>${i18nHelper.t("CPU_USAGE")}:</b> ${app.cpu}`;

                appCardBody.appendChild(appTitle);
                appCardBody.appendChild(appStatus);
                appCard.appendChild(appCardBody);
                appCol.appendChild(appCard);
                appRow.appendChild(appCol);
            });

            siteBody.appendChild(appRow);
            siteCard.appendChild(siteHeader);
            siteCard.appendChild(siteBody);
            siteCol.appendChild(siteCard);
            sitesRow.appendChild(siteCol);
        };
    }

    globalThis.g_bAuth = false
    globalThis.g_strAuthMech = ""

    var iChecks = 0;
    function DoCleanup( siteToCleanup = null )
    {
        let site = siteToCleanup;
        if( siteToCleanup === null )
            site = globalThis.g_rootSite;

        if( site.apps )
        {
            site.apps.forEach(app => {
                app.status = i18nHelper.t("APP_STATUS_UNKNOWN");
                app.cpu = "N/A";
            });
            if( site.children )
            {
                var taskGrp=[]
                
                site.children.forEach( ( oInfo, name ) => {
                    if( oInfo.site )
                        DoCleanup( oInfo.site );
                    if( oInfo.oProxy )
                    {
                        var oProxy = oInfo.oProxy
                        taskGrp.push( ()=>{
                        return oProxy.Stop( 0 ).then((e)=>{
                            console.log( `proxy ${oProxy.m_strRouterPath} stopped`)
                            return Promise.resolve(0);
                            }).catch((e)=>{
                                console.log( `proxy ${oProxy.m_strRouterPath} encountered error during stop ` + e)
                                return Promise.resolve(e);
                            })
                        })
                    }
                });
                async function stopProxies()
                {
                    for( const task of taskGrp ){
                        try{
                            await task()
                        }catch( e ){
                            return Promise.resolve( e )
                        }
                    }
                    return Promise.resolve( 0 )
                }
                stopProxies().then((e)=>{
                    site.children = new Map()
                    var curProxy = globalThis.curProxy
                    if( curProxy &&
                        GetSite( curProxy.m_strRouterPath ) === site )
                        updateSite(site);
                    if( site !== globalThis.g_rootSite )
                        return;

                    setStatusDot(false);
                    showReconnectButton(true, false);
                    clearSiteTree();

                    globalThis.curProxy = undefined;
                    var rootProxy = globalThis.rootProxy
                    globalThis.rootProxy = undefined;
                    globalThis.g_strLoginResult = ""
                    if( globalThis.curSpModal )
                    {
                        globalThis.curSpModal.hide();
                        globalThis.curSpModal = undefined;
                    }
                    initRootSite();
                    iChecks = 0;

                    if( rootProxy )
                    {
                        return rootProxy.Stop().then((e)=>
                        {
                            console.log( "rootProxy stopped" );
                        }).catch( (e)=>{
                            console.log( "Error stopping rootProxy" );
                        })
                    }
                }).catch((e)=>{
                    console.log( "Stop Child Client failed with " + e );
                    return Promise.resolve(-1);
                })
            }
        }
    }

    setStatusDot(false);
    showReconnectButton(true, false);

    function checkProxyState(){
        if( globalThis.rootProxy === undefined ){
            setTimeout( checkProxyState, 2000 );
            console.log( "Waiting RPC connection ready..." );
        }else if( globalThis.rootProxy === null ){
            console.log( "RPC connection is shutdown abnormally..." );
            DoCleanup();
        }else if( globalThis.rootProxy.m_iState === 0 ){
            console.log( "RPC connection is down..." );
            DoCleanup();
        }else{
            if( iChecks++ === 0 )
            {
                //fetchLogs();
                console.log( "RPC connection ready " + globalThis.curProxy );
            }
            setTimeout( checkProxyState, 5000 );
        }
    }
    setTimeout( checkProxyState, 2000 );

    function clearLogs()
    { document.getElementById('logs-panel').textContent = ""; }

// Toggle logs body (shows as a small link "[+]/[-]" in header)
    function toggleLogsPane(show) {
        const body = document.getElementById('logs-body');
        const link = document.getElementById('toggle-logs-link');
        if (!body || !link) return;
        if( !globalThis.rootProxy )
            show=false
        const visible = body.style.display !== 'none'
        if (typeof show === 'undefined') show = !visible;
        if (show) {
            body.style.display = '';
            link.textContent = '[-]';
        } else {
            body.style.display = 'none';
            link.textContent = '[+]';
        }
    }

        // Wire the link once DOM is ready
    document.addEventListener('DOMContentLoaded', () => {
        const link = document.getElementById('toggle-logs-link');
        if( link) link.addEventListener('click', (e) => {
            e.preventDefault();
            toggleLogsPane();
            const body = document.getElementById('logs-body');
            const visible = body.style.display !== 'none'
            if( visible )
                fetchLogs()
            });
    });

    function pollingBgSites()
    {
        if( !globalThis.rootProxy )
        {
          clearInterval(globalThis.pollingBgSitesInterval);
          globalThis.pollingBgSitesInterval = undefined;
          return Promise.resolve(0);
        }
        return globalThis.PollAllSites().then((e)=>{
          return Promise.resolve( 0 )
        }).catch(error=>{
          console.log("Error polling sites:", error);
          return Promise.resolve( error )
        })
    }
    function fetchStatus() {
      if( !globalThis.curProxy)
      {
          clearInterval(globalThis.fetchStatusInterval);
          globalThis.fetchStatusInterval = undefined;
          return Promise.resolve(0);
      }

      if( globalThis.funcStartPullInfo && globalThis.curProxy ){
          var startPullInfo = globalThis.funcStartPullInfo.bind( globalThis.curProxy );
          console.log( "Start pulling info..." );
          return startPullInfo().then((ret) => {
              updateSite();
          }).catch((error) => {
              console.log("Error fetching status:", error);
          });
        }
        return Promise.resolve(0);
    }
    function fetchLogs() {
        if( !globalThis.curProxy)
        {
            clearInterval(globalThis.fetchLogsInterval);
            globalThis.fetchLogsInterval = undefined;
            return Promise.resolve(0);
        }
        const body = document.getElementById('logs-body');
        const visible = body.style.display !== 'none'
        if (!visible) {
            // Skip fetching logs if not visible
            toggleLogsPane(false);
            return Promise.resolve(0);
        }
        // toggleLogsPane(true); // Ensure it's visible when fetching

        var oContext = Object()
        oContext.oGetLpvCb = (oContext, ret, value) => {
            const logsRow = document.getElementById('logs-row');
            if (ret === 0) {
                const decoder = new TextDecoder('utf-8')
                let text=decoder.decode(value);
                lineArray = text.split('\n').reverse();
                const regex = /^\[(\d+\.\d+)/;
                for(let i = 0; i < lineArray.length; i++) {
                  const match = lineArray[i].match(regex);
                  if (match) {
                      const timestamp = parseFloat(match[1]);
                      const date = new Date(timestamp * 1000);
                      let formattedDate = date.toLocaleString(
                          navigator.language,
                          { 
                              year: 'numeric',
                              month: '2-digit',
                              day: '2-digit',
                              hour: '2-digit',
                              minute: '2-digit',
                              second: '2-digit',
                              hour12: false, // Ensures 24-hour format
                          });
                      formattedDate = formattedDate.replace(/, /, '-');
                      lineArray[i] = lineArray[i].replace(regex, `[${formattedDate}`);
                  }
                }
                document.getElementById('logs-panel').innerHTML =
                    lineArray.join('\n').trim();
            } else {
                console.log("Error fetching logs:", ret);
            }
        };
        return globalThis.curProxy.GetLargePointValue(
            oContext, "loggersvr1/logcontent").then((ret) => {
            Promise.resolve(ret); 
        }).catch((error) => {
            console.log("Error fetching logs:", error);
        });
    }
    function showTimeSeriesModal(data, title = "Time Series") {
      // data: array of [timestamp, value]
      const ctx = document.getElementById('timeSeriesChart').getContext('2d');
      document.getElementById('timeSeriesModalLabel').textContent = title;

      // Format data for Chart.js
      const labels = data.map(pair => {
        const date = new Date(pair[0] * 1000); // assuming timestamp is in seconds
        return date.toLocaleString();
      });
      const values = data.map(pair => pair[1]);

      // Destroy previous chart if exists
      if( window.timeSeriesChart && window.timeSeriesChart.destroy)
      {
        window.timeSeriesChart.destroy();
      }

      window.timeSeriesChart = new Chart(ctx, {
        type: 'line',
        data: {
          labels: labels,
          datasets: [{
            label: title,
            data: values,
            borderColor: 'rgba(75,192,192,1)',
            backgroundColor: 'rgba(75,192,192,0.2)',
            tension: 0.1
          }]
        },
        options: {
          responsive: true,
          scales: {
            x: { display: true, title: { display: true, text: 'Time' } },
            y: { display: true, title: { display: true, text: 'Value' } }
          }
        }
      });

      // Prepare modal and backdrop
      var modalEl = document.getElementById('timeSeriesModal');
      modalEl.style.zIndex = 1070;

      // Add a custom backdrop above setpointsModal's backdrop
      let customBackdrop = document.createElement('div');
      customBackdrop.className = 'modal-backdrop fade show';
      customBackdrop.style.zIndex = 1069;
      customBackdrop.id = 'timeSeriesModal-backdrop';
      document.body.appendChild(customBackdrop);

      // Show the modal
      const modal = new bootstrap.Modal(document.getElementById('timeSeriesModal'));
      modal.show();

      // Cleanup: remove custom backdrop and reset z-index when closed
      modalEl.addEventListener('hidden.bs.modal', function handler() {
          modalEl.style.zIndex = '';
          let bd = document.getElementById('timeSeriesModal-backdrop');
          if (bd) bd.remove();
          globalThis.oStmCtx = null;
          modalEl.removeEventListener('hidden.bs.modal', handler);
      });
    }

    function showSetpvModal(app, pointName, currentValue) {
        document.getElementById('setpvModalLabel').textContent = i18nHelper.t("Set Point Values");
        document.getElementById('setpv-param').value = i18nHelper.t(pointName);
        document.getElementById('setpv-param').readOnly = true;
        document.querySelector('label[for="setpv-param"]').textContent = i18nHelper.t("Point Name");
        document.getElementById('setpv-value').value = currentValue;
        document.querySelector('label[for="setpv-value"]').textContent = i18nHelper.t("Value");
        document.getElementById('setpv-feedback').textContent = '';

        document.getElementById('setpv-form').onsubmit = function(e) {
            e.preventDefault();
            var newValue = document.getElementById('setpv-value').value;
            const { app, pointName } = globalThis.setpvContext || {};

            if( !app || !globalThis.curProxy )
                return;
            const fullPointPath = `${app.name}/${pointName}`;
            var oContext = Object();
            oContext.oSetPvCb = (oContext, ret) => { 
                if( oContext.m_iRet < 0 )
                {
                    document.getElementById('setpv-feedback').textContent = i18nHelper.t("CONFIG_UPDATE_FAILED");
                    console.log("Error setting point value:", oContext.m_iRet);
                }
                else
                    document.getElementById('setpv-feedback').textContent = i18nHelper.t("CONFIG_UPDATED");
            }
            const setpointMap = globalThis.curSpModal.currentApp.setpoints
            if ( !setpointMap || !setpointMap.has(pointName) )
            {
                document.getElementById('setpv-feedback').textContent = i18nHelper.t("CONFIG_UPDATE_FAILED");
                console.log("Setpoint not found:", pointName);
                return;
            }
            var iType = setpointMap.get(pointName).t;
            switch( iType )
            {
            case "byte":
            case "word":
            case "int":
            case "qword":
                var val = parseInt(newValue)
                if( isNaN( val ) )
                {
                    document.getElementById('setpv-feedback').textContent = i18nHelper.t("INVALID_INTEGER");
                    return;
                }
                newValue = val
                break;
            case "float":
            case "double":
                var val = parseFloat(newValue)
                if( isNaN( val ) )
                {
                    document.getElementById('setpv-feedback').textContent = i18nHelper.t("INVALID_FLOAT");
                    return;
                }
                newValue = val
                break;
            case "string":
                // keep as string
                break;
            case "blob":
                newValue = new TextEncoder().encode(newValue);
                break;
            case null:
            case undefined:
                return Promise.resolve(-22)
            }
            iType = str2type(iType);
            var oVar = globalThis.NewVariant( iType, newValue );
            return globalThis.curProxy.SetPointValue( oContext, fullPointPath, oVar ).then((ret)=>{
                setpointMap.get(pointName).v = newValue;
                setpointMap.get(pointName).cnt++;
                if( pointName === "display_name" )
                {
                    app.display_name = newValue;
                    updateSite();
                }
                setTimeout(() => {
                    bootstrap.Modal.getInstance(document.getElementById('setpvModal')).hide();
                }, 1000);
                return Promise.resolve(0)
            }).catch((error) => {
                setTimeout(() => {
                    bootstrap.Modal.getInstance(document.getElementById('setpvModal')).hide();
                }, 1000);
                return Promise.resolve(oContext.m_iRet )
            });
        };

        // Store context for submit
        globalThis.setpvContext = { app, pointName };

        var modalEl = document.getElementById('setpvModal');
        var modal = new bootstrap.Modal(modalEl, {backdrop: false});
        modalEl.style.zIndex = 1060;

        // Optionally, add a custom backdrop above the first modal's backdrop
        let customBackdrop = document.createElement('div');
        customBackdrop.className = 'modal-backdrop fade show';
        customBackdrop.style.zIndex = 1059;
        customBackdrop.id = 'setpvModal-backdrop';
        document.body.appendChild(customBackdrop);

        modal.show();

        // When setpvModal is hidden, remove the custom backdrop and reset z-index
        modalEl.addEventListener('hidden.bs.modal', function handler() {
            modalEl.style.zIndex = '';
            let bd = document.getElementById('setpvModal-backdrop');
            if (bd) bd.remove();
            modalEl.removeEventListener('hidden.bs.modal', handler);
        });
    }

    function showLogContextMenu(e, app, pointName, attr) {
        const menu = document.getElementById('logContextMenu');
        menu.style.display = 'block';
        menu.style.left = e.pageX + 'px';
        menu.style.top = e.pageY + 'px';

        // Remove any previous handlers
        menu.querySelectorAll('.dropdown-item').forEach(item => {
            item.onclick = null;
        });

        // Add click handlers for each menu item
        menu.querySelectorAll('.dropdown-item').forEach(item => {
            item.onclick = function(ev) {
                ev.preventDefault();
                menu.style.display = 'none';
                let range = this.getAttribute('data-range');
                // Call your log download and showTimeSeriesModal logic here
                let timeRangeSec = 0;
                if (range === '1h') timeRangeSec = 400;
                else if (range === '2h') timeRangeSec = 4000;
                else if (range === '3h') timeRangeSec = 40000;
                else timeRangeSec = 0; // 0 means full log

                var oContext = {};
                globalThis.downloadPointLog(
                    oContext,
                    `${app.name}/${pointName}`,
                    "ptr0-0",
                    attr.avgalgo,
                    timeRangeSec // Pass time range in seconds if your backend supports it
                ).then((ret) => {
                    if (oContext.m_iRet < 0)
                        return Promise.resolve(oContext.m_iRet);
                    var arrTimeSeries = globalThis.parseLogLines(
                        oContext.m_arrLogData, oContext.m_avgAlgo,
                        oContext.m_timeRangeSec );
                    if (arrTimeSeries.length)
                        showTimeSeriesModal(
                            arrTimeSeries,
                            `${getDisplayName(app)}/${i18nHelper.t(pointName)}`
                        );
                    return Promise.resolve(0);
                }).catch((error) => {
                    console.log("Error downloading point log:", error);
                    return Promise.resolve(-1);
                });
            };
        });
    }

    // Show app context menu and wire actions
    function showAppContextMenu(e, app, site) {
        const menu = document.getElementById('appContextMenu');
        menu.style.display = 'block';
        menu.style.left = e.pageX + 'px';
        menu.style.top = e.pageY + 'px';

        // clear previous handlers
        menu.querySelectorAll('.dropdown-item').forEach(item => item.onclick = null);
        var appName = app.name
        var routerPath = site.routePath

        // Configure menu items: disable start when running, disable stop when stopped
        menu.querySelectorAll('.dropdown-item').forEach(item => {
            const action = item.getAttribute('data-action');

            var curSite = GetSite( routerPath )
            if( !curSite )
                return
            var curApp;
            curSite.apps.forEach( elem => {
                if( elem.name === appName )
                    curApp = elem
            })
            if( !curApp )
                return
            // Determine app state using i18n strings (matches how statuses are set)
            const isRunning = (curApp && curApp.status === i18nHelper.t("APP_STATUS_RUNNING"));
            const isStopped = (curApp && curApp.status === i18nHelper.t("APP_STATUS_STOPPED"));

            if ((action === 'start' && isRunning) || (action === 'stop' && isStopped)) {
                item.classList.add('disabled');
                item.setAttribute('aria-disabled', 'true');
                item.tabIndex = -1;
                item.onclick = null;
            } else {
                item.classList.remove('disabled');
                item.removeAttribute('aria-disabled');
                item.tabIndex = 0;
                // attach handler
                item.onclick = function(ev) {
                    ev.preventDefault();
                    menu.style.display = 'none';
                    handleAppAction(action, curApp, curSite);
                };
            }
        })
    }

    // Perform the selected action; adapt to your backend API
    function handleAppAction(action, app) {

        const oProxy = globalThis.curProxy;

        // Try common method names; adjust to your actual API
        if (!oProxy) {
            console.warn('No proxy available for', routerPath);
            return;
        }

        // Map actions to proxy methods
        const actionMap = {
            start: 1,
            stop:  2,
            restart: 3
        };

        const cmdId = actionMap[action] ;

        if (cmdId) {
            var oContext = {};
            oProxy.SetPointValue( oContext,
                `${app.name}/restart`, 
                globalThis.NewVariant( str2type("int"), cmdId )
            ).then((ret) => {
                console.log(`App ${action} command sent for`, app.name);
            }).catch((err) => console.error(`Error sending ${action} command for`, app.name, err));
        }
    }

    // hide menus when clicking elsewhere (keep existing logContextMenu hide)
    document.addEventListener('click', function() {
        const m1 = document.getElementById('logContextMenu');
        const m2 = document.getElementById('appContextMenu');
        if (m1) m1.style.display = 'none';
        if (m2) m2.style.display = 'none';
    });

    function BuildSetpointCell( app, pointName, value, rowIndex )
    {
        const tr = document.createElement('tr');
        /*if (rowIndex % 2 === 1) {
            tr.style.backgroundColor = "#2c2f33"; // dark gray, or any color you like
        }*/
        const nameTd = document.createElement('td');
        nameTd.textContent = i18nHelper.t(pointName);
        tr.appendChild(nameTd);

        var valToShow = value.v;
        if( ( valToShow != null && valToShow != undefined )&&
            ( value.t === "float" || value.t === "double") )
            valToShow = valToShow.toFixed(3);
        if( value.cnt > 0 )
        {
            valColor = "text-danger";
            value.cnt=0;
        }
        else
            valColor = "text-secondary";
        if( value.v === null || value.v === undefined )
            valToShow = "N/A";
        if( value.v !== null && value.v !== undefined &&
            value.unit && value.unit.length > 0 )
            valToShow = `${valToShow} ${i18nHelper.t(value.unit)}`;
        valToShow = `<span class="${valColor}">${valToShow}</span>`;
        const valueTd = document.createElement('td');
        valueTd.innerHTML = valToShow;
        valueTd.style.cursor = "pointer";
        if( value.ptype && value.ptype === "setpoint" )
        {
          valueTd.onclick = function() {
              showSetpvModal(app, pointName, value.v);
          };
        }
        else if( value.ptype && value.ptype === "output" )
        {
            var setpoints = app.setpoints
            var attr = setpoints.get(pointName)
            if( attr && attr.haslog )
            {
                valueTd.oncontextmenu = function(e) {
                    e.preventDefault();
                    showLogContextMenu(e, app, pointName, attr);
                };

                // Add a small log icon to the right
                const logIcon = document.createElement('i');
                logIcon.className = "bi bi-clipboard-data ms-2 text-info"; // Bootstrap icon with margin and color
                logIcon.title = i18nHelper.t("View Log Data");
                valueTd.appendChild(logIcon);
            }
        }

        tr.appendChild(valueTd);

        const typeTd = document.createElement('td');
        typeTd.textContent = i18nHelper.t(value.ptype, {},"setpoint");
        tr.appendChild(typeTd);
        return tr;
    }

    function updateAppDetailModal(app, site) {
        // Set modal title
        document.getElementById('setpointsModalLabel').textContent = i18nHelper.t("Setpoints");
        document.getElementById('setpointsAppName').textContent = `${i18nHelper.t("APP")}: ${getDisplayName(app)} (${i18nHelper.t("SITE_LABEL")}: ${site.name})`;
        document.getElementById('setpointsAppName').style.fontStyle = "italic";
        const setpoints = app.setpoints 
        const tbody = document.getElementById('setpointsTableBody');
        tbody.innerHTML = '';
        if (setpoints.size === 0) {
            tbody.innerHTML = `<tr><td colspan="2">${i18nHelper.t("NO_DATA")}</td></tr>`;
        } else {
            const arrPts = globalThis.curSpModal.arrPts;
            arrPts.forEach((elem, idx) =>
            {
                if( !setpoints.has(elem.n) )
                    return
                value = setpoints.get(elem.n);
                if( value === undefined )
                    return
                const tr = BuildSetpointCell( app, elem.n, value, idx );
                tbody.appendChild(tr);
            })
        }
    }
    function showAppDetailModal(app, site) {
        // Set modal title
        document.getElementById('setpointsModalLabel').textContent = i18nHelper.t("Setpoints");
        document.getElementById('setpointsAppName').textContent = `${i18nHelper.t("APP")}: ${getDisplayName(app)} (${i18nHelper.t("SITE_LABEL")}: ${site.name})`;

        // Example: app.setpoints = [{name: "SP1", value:{ v:123, t:"i" }, ...}]
        const setpoints = app.setpoints || new Map();
        const tbody = document.getElementById('setpointsTableBody');
        tbody.innerHTML = '';
        if (setpoints.size === 0) {
            tbody.innerHTML = `<tr><td colspan="2">${i18nHelper.t("NO_DATA")}</td></tr>`;
        } else {
            var idx = 0;
            setpoints.forEach((value, key) => {
                const tr = BuildSetpointCell( app, key, value, idx++ );
                tbody.appendChild(tr);
            });
        }

        // Show the modal
        var modalEl = document.getElementById('setpointsModal');
        var modal = new bootstrap.Modal(modalEl);
        globalThis.curSpModal = modal;
        modal.currentSite = site;
        modal.currentApp = app;

        globalThis.OnPointChanged = ( oProxy, strPtPath, value) => {
            if( globalThis.curProxy !== oProxy )
                return
            if (modal.currentApp && modal.currentApp.setpoints) {
                var arrComps = strPtPath.split('/')
                if( arrComps.length < 2 )
                    return
                attr = modal.currentApp.setpoints.get(arrComps[1])
                if (!attr)
                    attr = { v: null, t: null, unit: null, cnt: 0 };
                attr.v = value.m_val
                attr.t = type2str(value.m_iType)
                attr.cnt++ 
                modal.currentApp.setpoints.set(arrComps[1], attr);
            }
        };
        modalEl.addEventListener('hidden.bs.modal', function spthandler(event) {
            document.getElementById('setpointsTableBody').innerHTML = '';
            modalEl.removeEventListener('hidden.bs.modal', spthandler);
            if( globalThis.curSpModal && globalThis.curProxy)
            {
                return globalThis.curProxy.RemoveListener( Object() ).then((ret)=>{
                    var modal = globalThis.curSpModal;
                    if ( !modal )
                        return;
                    modal.currentApp.bRegistered = false;
                    if( modal.currentApp )
                        modal.currentApp.setpoints = new Map();
                    console.log("Listener removed:", ret);
                    globalThis.curSpModal = null;
                    globalThis.fetchLogsInterval =
                        setInterval(fetchLogs, 30000);
                }).catch((error)=>{
                    var modal = globalThis.curSpModal;
                    if ( !modal )
                        return;
                    modal.currentApp.bRegistered = false;
                    if( modal.currentApp )
                        modal.currentApp.setpoints = new Map();
                    console.log("Error removing listener:", error);
                    globalThis.curSpModal = null;
                    globalThis.fetchLogsInterval =
                        setInterval(fetchLogs, 30000);
                });
            }
        });

        document.getElementById('setpoint-name-header').textContent =
            i18nHelper.t("Name");
        document.getElementById('setpoint-value-header').textContent =
            i18nHelper.t("Value");
        document.getElementById('setpoint-type-header').textContent =
            i18nHelper.t("Point Type", {}, "setpoint");
        modal.show();
        clearInterval(globalThis.fetchLogsInterval);
        globalThis.fetchLogsInterval = undefined;
        fetchStatus();
    }

    globalThis.StartClient( oStartCb)

/*
  Render a dropdown tree from the hierarchical backend (g_rootSite or rootProxy).
  Clicking a site node will switch the current view to that site's router path.
*/
function getChildrenArray(node) {
  if (!node) return [];
  var o=[]
  if (typeof node.children === 'object' && node.children !== null)
      node.children.forEach( ( value, strKey )=>{
          o.push( { key: strKey, node : value } )
      })
  return o;
}

function renderSiteTree(rootNode) {
  const container = document.getElementById('siteTreeContainer');
  if (!container) return;
  container.innerHTML = '';

  function buildNode(elem, path, offline=false) {
    const li = document.createElement('li');
    li.style.listStyle = 'none';
    li.style.padding = '2px 4px';

    const label = document.createElement('div');
    label.className = 'd-flex justify-content-between align-items-center';
    label.style.cursor = 'pointer';
    label.textContent = elem.name
    if( offline )
        label.style.color = 'gray';

    // compute routerPath for this node
    const routerPath = path

    // If this element holds a site object, attach click handler to select it
    const siteObj = elem;
    if (siteObj ) {
        label.onclick = function (e) {
            e.stopPropagation();
            const routerPath = computeRouterPathFromNode(elem)
            if (routerPath) {
                selectSiteByRouterPath(routerPath);
                // close dropdown
                const btn = document.getElementById('siteTreeDropdownBtn');
                const dd = bootstrap.Dropdown.getInstance(btn);
                if (dd) dd.hide();
            }
        };
    } else {
        // folder node: toggle children
        label.onclick = function(e) {
            e.stopPropagation();
            li.classList.toggle('open');
            const sub = li.querySelector('ul');
            if (sub) sub.style.display = sub.style.display === 'none' ? 'block' : 'none';
        };
    }
    li.appendChild(label);

    // children
    const childUl = document.createElement('ul');
    childUl.style.paddingLeft = '0.6rem';
    childUl.style.margin = '0';
    // childUl.style.display = 'none';
    li.appendChild(childUl);

    const children = getChildrenArray(elem.node);
    if (children.length) {
      children.forEach((c, idx) => {
        // normalize child element shape
        let childElem = c;
        if (c.key !== undefined && c.node !== undefined)
          childElem = { key: c.key, node: c.node, name: c.key };
        else if (c.name) childElem = c;
        const delimiter = (path === '/' ) ? '' : '/';
        var offline = false
        if( !c.node.oProxy )
            offline = true
        childUl.appendChild(buildNode(childElem, path ? (path + delimiter + (childElem.name || childElem.key)) : (childElem.name || childElem.key), offline ));
      });
    }
    return li;
  }

  const rootUl = document.createElement('ul');
  rootUl.style.paddingLeft = '0';
  // If rootNode is an object with children mapping, convert to consistent element
  let rootElem = { node: rootNode,
      name: rootNode.name === '/' ? 'Root' : rootNode.name };
  rootUl.appendChild(buildNode(rootElem, rootNode.routerPath ));
  container.appendChild(rootUl);
}

// Attempt to compute routerPath from node element by walking up from name strings.
// This utility assumes the tree labels represent path components and returns a path string.
function computeRouterPathFromNode(elem) {
  // If the node already has a params object with router path, try to use it
  return elem.node.routerPath || elem.node.site.routerPath ;
}
/*
 * Select site by routerPath WITHOUT starting clients.
 * Assumes GetProxy/GetSite work against the pre-built hierarchy.
 */
function selectSiteByRouterPath(routerPath) {
    if (!routerPath) return;

    // if already selected, just re-render
    if (globalThis.curProxy && globalThis.curProxy.m_strRouterPath === routerPath) {
        const site = globalThis.GetSite(routerPath);
        if (site) renderSite(site);
        return;
    }

    // Try to get proxy and site directly from pre-built structures
    const proxy = (typeof globalThis.GetProxy === 'function') ? globalThis.GetProxy(routerPath) : null;
    const site = (typeof globalThis.GetSite === 'function') ? globalThis.GetSite(routerPath) : null;

    // Update current proxy and render site (proxy may be null if not available)
    globalThis.curProxy = proxy || null;
    if (site) {
        renderSite(site);
    } else {
        console.warn('selectSiteByRouterPath: site not found for', routerPath);
    }

    // refresh status/logs state for new curProxy
    if (globalThis.curProxy) {
        // ensure status/logs polling targets the new proxy
        clearInterval(globalThis.fetchStatusInterval);
        clearInterval(globalThis.fetchLogsInterval);
        globalThis.fetchStatusInterval = setInterval(fetchStatus, 5000);
        globalThis.fetchLogsInterval = setInterval(fetchLogs, 30000);
        fetchStatus();
        fetchLogs();
    } else {
        // no proxy: stop polling and clear display
        clearInterval(globalThis.fetchStatusInterval);
        clearInterval(globalThis.fetchLogsInterval);
        setStatusDot(false);
        showReconnectButton(true, false);
    }
}
function initSiteTreeButtons() {
  document.getElementById('expandAllSitesBtn').onclick = () => {
    document.querySelectorAll('#siteTreeContainer ul').forEach(u => u.style.display = 'block');
  };
  document.getElementById('collapseAllSitesBtn').onclick = () => {
    document.querySelectorAll('#siteTreeContainer ul').forEach(u => u.style.display = 'none');
  };
}

// initialize site tree when data ready
function initSiteTree() {
  const rootNode = globalThis.g_rootSite;
  if (!rootNode) return;
  renderSiteTree(rootNode);
  // initSiteTreeButtons();
  renderSite(rootNode);
  globalThis.pollingBgSitesInterval = setInterval( pollingBgSites, 40000);
}

// call initSiteTree after StartClient / data loaded
// setTimeout(() => { initSiteTree(); }, 500);
  ////# sourceURL=rpcf://htmlcode/appmonhtml.js
  </script>
  <p id="rpcf_load_notify"><br /></p>
</body>
</html>
